package com.javapda.c328r.util;

import gnu.io.CommPort;
import gnu.io.CommPortIdentifier;
import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import com.javapda.c328r.C328rCameraConfiguration;
import com.javapda.c328r.command.ComediaCommand;
import com.javapda.c328r.command.ComediaDataPackage;
import com.javapda.c328r.command.ComediaLightFrequencyType;
import com.javapda.c328r.command.ComediaPictureType;
import com.javapda.c328r.command.ComediaPreviewColorType;
import com.javapda.c328r.command.ComediaSnapshotType;
import com.javapda.c328r.command.impl.AckComediaCommand;
import com.javapda.c328r.command.impl.AckComediaCommandId;
import com.javapda.c328r.command.impl.DataComediaCommand;
import com.javapda.c328r.command.impl.GenericComediaCommandId;
import com.javapda.c328r.command.impl.GetPictureComediaCommand;
import com.javapda.c328r.command.impl.InitialComediaCommand;
import com.javapda.c328r.command.impl.NakComediaCommand;
import com.javapda.c328r.command.impl.NakComediaCommandId;
import com.javapda.c328r.command.impl.PowerOffComediaCommand;
import com.javapda.c328r.command.impl.RequestEndPackageDataTransferComediaCommand;
import com.javapda.c328r.command.impl.RequestPackageDataComediaCommand;
import com.javapda.c328r.command.impl.SetLightFrequencyComediaCommand;
import com.javapda.c328r.command.impl.SetPackageSizeComediaCommand;
import com.javapda.c328r.command.impl.SnapshotComediaCommand;
import com.javapda.c328r.command.impl.SyncComediaCommand;
import com.javapda.util.ByteUtil;
import com.javapda.util.FileUtil;

public class SyncCam {
	
	
	private static final int COMEDIA_SYNC_MAX_TRIES = 60;

	public static void main(String[] args) {
		String comPort = "COM5";
		if ( System.getProperty("os.name").indexOf("Windows") != -1 ) {
			System.out.println("COMPUTERNAME: " + System.getenv("COMPUTERNAME"));
			if ( System.getenv("COMPUTERNAME").equals("JGKVISTA") ) {
				comPort = "COM1";
			}
		} else {
			System.out.println("HOSTNAME: " + System.getenv("HOSTNAME"));
		}
		new SyncCam().sync(comPort);
	}

	private void sync(String portName) {
		try {
			CommPortIdentifier portIdentifier = CommPortIdentifier
			.getPortIdentifier(portName);
			CommPort commPort = portIdentifier.open(this.getClass().getName(),
							2000);
			System.out.println("PORT: " + commPort.getName());
			if (commPort instanceof SerialPort) {
				SerialPort serialPort = (SerialPort) commPort;
				setupSerialPort(serialPort);
				syncHostWithCamera(serialPort);
				initializeCamera(serialPort);
				setLightFrequency(serialPort);
				setPackageSize(serialPort);
				long start = System.currentTimeMillis();
				int numPics = 1;
				for (int i = 0; i < numPics; i++) {
					pause(100);  // introduce pause to allow camera to stabilize settings before picture taken
					snapshot(serialPort);
					getPicture(serialPort);
				}
				long duration = System.currentTimeMillis()-start;
				System.out.println("DURATION: "+((double)duration)/1000 + " sec");
				System.out.println("Frame rate: " + (numPics*1000./duration) + " f/s");
				powerOff(serialPort);
				serialPort.close();
			} else {
				System.out.println("OUCH");
			}
		} catch (IOException e) {
			e.printStackTrace();
		} catch (UnsupportedCommOperationException e) {
			e.printStackTrace();
		} catch (PortInUseException e) {
			e.printStackTrace();
		} catch (NoSuchPortException e) {
			e.printStackTrace();
		}
		
		
	}

	private void setLightFrequency(SerialPort serialPort) throws IOException {
		SetLightFrequencyComediaCommand cmd = new SetLightFrequencyComediaCommand(ComediaLightFrequencyType.FREQ_50_HZ);
		sendCommandToCamera(serialPort, cmd);
		ComediaCommand response=getAckNakFromCamera(serialPort);
		if ( isAck(response) ) {
			// All is well
			System.out.println("HURAY, CAMERA Set LIGHT FREQUENCY");
		} else {
			// Problem 
			throw new RuntimeException("Problem NAK: " + response);
		}
		
	}

	private void powerOff(SerialPort serialPort) throws IOException {
		PowerOffComediaCommand cmd = new PowerOffComediaCommand();
		sendCommandToCamera(serialPort, cmd);
		System.out.println("Powered off");
	}

	private void getPicture(SerialPort serialPort) throws IOException {
		GetPictureComediaCommand cmd = new GetPictureComediaCommand();
		cmd.setPictureType(ComediaPictureType.SNAPSHOT);
		sendCommandToCamera(serialPort, cmd);
		pause(100);
		// get an ack/nak
		ComediaCommand response=getAckNakFromCamera(serialPort);
		if ( isAck(response) ) {
			// All is well
//			System.out.println("HURAY, CAMERA GET PICTURE");
		} else {
			// Problem 
			throw new RuntimeException("Problem NAK: " + response);
		}
		receiveData(serialPort);
	}
	private int[] readComediaCommand(SerialPort serialPort) throws IOException {
		InputStream in = serialPort.getInputStream();
		int[] nums = new int[6];
		for (int i = 0; i < nums.length; i++) {
			nums[i] = ByteUtil.readOneByte(in);
		}
		return nums;
	}

	private void receiveData(SerialPort serialPort) throws IOException {
		InputStream in = serialPort.getInputStream();
		int[] nums = readComediaCommand(serialPort);
		DataComediaCommand cmd = new DataComediaCommand(nums);
		sendAckToCamera(serialPort);
		// retrieve bytes of Data 
		int expectedImageSize = cmd.getImageSize();
//		System.out.println("@@ IMAGE SIZE: " + expectedImageSize);
		int numPackages = (int) Math.ceil(cmd.getImageSize()/506.0);
//		int numPackages = (int) Math.ceil(cmd.getImageSize()/512.0);
//		System.out.println("No. Packages (CEIL)="+numPackages);
		List comediaDataPackageList = new ArrayList();
		
		for (int i = 0; i < numPackages; i++) {
			pause(100);
				// read in package id
				int[] packageIdBytes = { ByteUtil.readOneByte(in), ByteUtil.readOneByte(in) };
				int packageId = ByteUtil.intArrayToInt(packageIdBytes);
				
				// read in data size
				int[] imageDataSizeBytes = { ByteUtil.readOneByte(in), ByteUtil.readOneByte(in) };
				int imageDataSize = ByteUtil.intArrayToInt(imageDataSizeBytes);
				
				// read data size worth of data
				byte[] imageData = new byte[imageDataSize];
//				System.out.println("READING: imageDataSize="+imageDataSize);
				for (int j = 0 ; j < imageDataSize ; j++ ) {
					imageData[j]=(byte) ByteUtil.readOneByte(in);
				}
//				in.read(imageData);
				
				// read in verify error
				int[] verifyErrorBytes = { ByteUtil.readOneByte(in), ByteUtil.readOneByte(in) };
				int verifyError = ByteUtil.intArrayToInt(verifyErrorBytes);
				
			ComediaDataPackage cdp = new ComediaDataPackage();
			cdp.setPackageId(packageId);
			cdp.setImageData(imageData);
			cdp.setVerifyError(verifyError);
//			System.out.println("Package ID="+cdp.getPackageId());
			comediaDataPackageList.add(cdp);
			RequestPackageDataComediaCommand reqPackageComand = new RequestPackageDataComediaCommand();
			int nextPackageId = i+1;
			reqPackageComand.setPackageId(nextPackageId);
			sendCommandToCamera(serialPort, reqPackageComand);
		}
		// walk through all items and see what actual data size is
		byte[] imageBytes = convertImageDataToByteArray(comediaDataPackageList);
		writeBytesToFile(imageBytes);
		RequestEndPackageDataTransferComediaCommand endCommand = new RequestEndPackageDataTransferComediaCommand();
		sendCommandToCamera(serialPort, endCommand);
	}

	private byte[] convertImageDataToByteArray(List dt)
					throws IOException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		for (Iterator iterator = dt.iterator(); iterator.hasNext();) {
			ComediaDataPackage cdp = (ComediaDataPackage) iterator.next();
//			actualImageSize += cdp.getImageDataSize();
			baos.write(cdp.getImageData());
		}
		return baos.toByteArray();
	}

	private void writeBytesToFile(byte[] imageBytes) {
		Date date = new Date();
		DateFormat df = new SimpleDateFormat("yyyyMMdd_HHmmss");
		String fileName = "C328R_" + df.format(date) + "_"+System.currentTimeMillis()+".jpg";
		File file = new File(".",fileName);
		try {
			FileUtil.writeBytesToFile(imageBytes, file);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private void sendAckToCamera(SerialPort serialPort, int packageId) throws IOException {
		// send ACK to camera
		AckComediaCommand ack = new AckComediaCommand();
		ack.setParameter3(packageId);
		sendCommandToCamera(serialPort, ack);
	}
	private void sendAckToCamera(SerialPort serialPort) throws IOException {
		// send ACK to camera
		AckComediaCommand ack = new AckComediaCommand();
		sendCommandToCamera(serialPort, ack);
	}

	private void snapshot(SerialPort serialPort) throws IOException {
		SnapshotComediaCommand cmd = new SnapshotComediaCommand();
		cmd.setSnapshotType(ComediaSnapshotType.COMPRESSED);
		// no skip frame
		cmd.setSkipFrameHighByte(0x00);
		cmd.setSkipFrameLowByte(0x00);
		sendCommandToCamera(serialPort, cmd);
		// get an ack/nak
		ComediaCommand response=getAckNakFromCamera(serialPort);
		while ( response == null ) {
			System.out.println("SNAPSHOT ACK/NAK again");
			response=getAckNakFromCamera(serialPort);
		}
		if ( isAck(response) ) {
			// All is well
//			System.out.println("HURAY, CAMERA SNAPSHOT");
		} else {
			// Problem 
			throw new RuntimeException("Problem NAK: " + response);
		}
	}

	private void setPackageSize(SerialPort serialPort) throws IOException {
		// simplify the API here
		SetPackageSizeComediaCommand cmd = new SetPackageSizeComediaCommand();
		// 512 bytes, 0x0200, low=0x00 & high=0x02
		cmd.setPackageSizeLowByte(0x00);
		cmd.setPackageSizeHighByte(0x02);
		sendCommandToCamera(serialPort, cmd);
		// get an ack/nak
		ComediaCommand response=getAckNakFromCamera(serialPort);
		if ( isAck(response) ) {
			// All is well
			System.out.println("HURAY, CAMERA SET PACKAGE SIZE");
		} else {
			// Problem 
			throw new RuntimeException("Problem NAK: " + response);
		}
		
	}

	private void initializeCamera(SerialPort serialPort) throws IOException {
		OutputStream out = serialPort.getOutputStream();
		// send the init
		InitialComediaCommand cmd = new InitialComediaCommand();
		cmd.setPreviewColorType(ComediaPreviewColorType.JPEG);
		// don't care about preview resolution
//		cmd.setPreviewJpegResolution(ComediaPreviewJpegResolution.RES_80x64);
//		cmd.setPreviewJpegResolution(ComediaPreviewJpegResolution.RES_160x128);
//		cmd.setPreviewJpegResolution(ComediaPreviewJpegResolution.RES_320x240);
//		cmd.setPreviewJpegResolution(ComediaPreviewJpegResolution.RES_640x480);
		C328rCameraConfiguration cfg = new C328rCameraConfiguration();
		cmd.setPreviewJpegResolution(cfg.getPreviewJpegResolution());
		cmd.setPreviewColorType(cfg.getPreviewColorType());
		sendCommandToCamera(serialPort,cmd);
		
		// get an ack/nak
		ComediaCommand response=getAckNakFromCamera(serialPort);
		if ( isAck(response) ) {
			// All is well
			System.out.println("HURAY, CAMERA INITIALIZED");
		} else {
			// Problem 
			throw new RuntimeException("Problem NAK: " + response);
		}
		
		
	}

	private boolean isAck(ComediaCommand comediaCommand) {
		return comediaCommand.getData()[1] == 0x0E;
	}

	private ComediaCommand getAckNakFromCamera(SerialPort serialPort) throws IOException {
		InputStream in = serialPort.getInputStream();
		int[] nums = {5,5,5,5,5,5};
		for (int i = 0; i < nums.length; i++) {
			nums[i]=ByteUtil.readOneByte(in);
		}
		GenericComediaCommandId gccid = new GenericComediaCommandId(nums[0],nums[1]);
		ComediaCommand response = null;
		if ( (new AckComediaCommandId()).equals(gccid) ) {
			response = new AckComediaCommand(nums);
		} else if ((new NakComediaCommandId()).equals(gccid)){
			response = new NakComediaCommand(nums);
		} else {
			System.out.println("NO MATCH ACK/NAK " + gccid);
		}
		return response;
	}

	private void sendCommandToCamera(SerialPort serialPort,
					ComediaCommand cmd) throws IOException {
		OutputStream out = serialPort.getOutputStream();
		for (int i = 0; i < cmd.getData().length; i++) {
			out.write(cmd.getData()[i]);
		}
		
	}

	private boolean syncHostWithCamera(SerialPort serialPort)
					throws IOException {
		boolean result = false;
		for ( int i = 0 ; i < COMEDIA_SYNC_MAX_TRIES && result == false; i++) {
			System.out.print("Attempt " + (i+1) + "...");
			result = doWork(serialPort);
//					System.out.println("Result = " + result);
		}
		return result;
	}

	private boolean doWork(SerialPort serialPort) throws IOException {
		boolean result = false;
		InputStream in = serialPort.getInputStream();
		OutputStream out = serialPort.getOutputStream();
		SyncComediaCommand cmd = new SyncComediaCommand();
//		System.out.println(cmd.getDataAsString());
//		System.out.println(cmd.getData().length);
		int[] data = cmd.getData();
		for (int i = 0; i < data.length; i++) {
//			System.out.println("Write: " + data[i]);
			out.write(data[i]);
		}
		pause(1000);
		int num = 0;
		int[] nums = new int[6];
		num = in.read();
		if (num!=-1) {
			result=true;
			nums[0] = num;
			for (int i = 1; i < nums.length; i++) {
				num=in.read();
				nums[i] = num;
			}
		} else {
			// nothing read move on
		}
		
		GenericComediaCommandId gccid = new GenericComediaCommandId(nums[0],nums[1]);
		if ( (new AckComediaCommandId()).equals(gccid) ) {
			System.out.println("MATCH ACK");
			AckComediaCommand ack = new AckComediaCommand(nums);
			System.out.println("ACK: " + ack);
			getSyncFromCamera(in,out);
		} else if ((new NakComediaCommandId()).equals(gccid)){
			System.out.println("OUCH, it's a NAK");
			NakComediaCommand nak = new NakComediaCommand(nums);
			System.out.println("NAK: " + nak);
		} else {
			System.out.println("NO MATCH ACK "+gccid);
		}
		
		
		return result;
	}

	private void getSyncFromCamera(InputStream in, OutputStream out) throws IOException {
		System.out.println("GETTING SYNC FROM CAM");
		int[] nums = new int[6];
		int num = in.read();
		if (num!=-1) {
//			result=true;
			nums[0] = num;
			for (int i = 1; i < nums.length; i++) {
				num=in.read();
				nums[i] = num;
			}
			for (int i = 0; i < nums.length; i++) {
				System.out.println(nums[i]);
			}
			// send ACK to camera, with this id
			System.out.println("TIME TO SEND ACK TO CAMERA");
			AckComediaCommand ack = new AckComediaCommand();
			ack.setCmdId(nums[1]);
			ComediaCommand cmd = ack;
			int[] data = cmd.getData();
			for (int i = 0; i < data.length; i++) {
//				System.out.println("Write: " + data[i]);
				out.write(data[i]);
			}
		}		
	}

	private void pause(long millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
	}

	private void setupSerialPort(SerialPort serialPort) throws UnsupportedCommOperationException {
		int baudrate = 115200; //57600;
		int dataBits = SerialPort.DATABITS_8;
		int stopBits = SerialPort.STOPBITS_1;
		int parity = SerialPort.PARITY_NONE;
		serialPort.setSerialPortParams(baudrate,dataBits,stopBits,parity);
		System.out.println("Serial Port initialized");

		
	}
}
